/*!
 * Globalize
 *
 * http://github.com/jquery/globalize
 *
 * Copyright Software Freedom Conservancy, Inc.
 * Dual licensed under the MIT or GPL Version 2 licenses.
 * http://jquery.org/license
 */

(function(window, undefined){

    var Globalize,
        // private variables
        regexHex,
        regexInfinity,
        regexParseFloat,
        regexTrim,
        // private JavaScript utility functions
        arrayIndexOf,
        endsWith,
        extend,
        isArray,
        isFunction,
        isObject,
        startsWith,
        trim,
        truncate,
        zeroPad,
        // private Globalization utility functions
        appendPreOrPostMatch,
        expandFormat,
        formatDate,
        formatNumber,
        getTokenRegExp,
        getEra,
        getEraYear,
        parseExact,
        parseNegativePattern;

    // Global variable (Globalize) or CommonJS module (globalize)
    Globalize = function(cultureSelector){
        return new Globalize.prototype.init(cultureSelector);
    };

    if(typeof require !== "undefined" &&
        typeof exports !== "undefined" &&
        typeof module !== "undefined") {
        // Assume CommonJS
        module.exports = Globalize;
    } else {
        // Export as global variable
        window.Globalize = Globalize;
    }

    Globalize.cultures = {};

    Globalize.prototype = {
        constructor: Globalize,
        init: function(cultureSelector){
            this.cultures = Globalize.cultures;
            this.cultureSelector = cultureSelector;

            return this;
        }
    };
    Globalize.prototype.init.prototype = Globalize.prototype;

    // 1. When defining a culture, all fields are required except the ones stated as optional.
    // 2. Each culture should have a ".calendars" object with at least one calendar named "standard"
    //    which serves as the default calendar in use by that culture.
    // 3. Each culture should have a ".calendar" object which is the current calendar being used,
    //    it may be dynamically changed at any time to one of the calendars in ".calendars".
    Globalize.cultures["default"] = {
        // A unique name for the culture in the form <language code>-<country/region code>
        name: "en",
        // the name of the culture in the english language
        englishName: "English",
        // the name of the culture in its own language
        nativeName: "English",
        // whether the culture uses right-to-left text
        isRTL: false,
        // "language" is used for so-called "specific" cultures.
        // For example, the culture "es-CL" means "Spanish, in Chili".
        // It represents the Spanish-speaking culture as it is in Chili,
        // which might have different formatting rules or even translations
        // than Spanish in Spain. A "neutral" culture is one that is not
        // specific to a region. For example, the culture "es" is the generic
        // Spanish culture, which may be a more generalized version of the language
        // that may or may not be what a specific culture expects.
        // For a specific culture like "es-CL", the "language" field refers to the
        // neutral, generic culture information for the language it is using.
        // This is not always a simple matter of the string before the dash.
        // For example, the "zh-Hans" culture is netural (Simplified Chinese).
        // And the "zh-SG" culture is Simplified Chinese in Singapore, whose lanugage
        // field is "zh-CHS", not "zh".
        // This field should be used to navigate from a specific culture to it's
        // more general, neutral culture. If a culture is already as general as it
        // can get, the language may refer to itself.
        language: "en",
        // numberFormat defines general number formatting rules, like the digits in
        // each grouping, the group separator, and how negative numbers are displayed.
        numberFormat: {
            // [negativePattern]
            // Note, numberFormat.pattern has no "positivePattern" unlike percent and currency,
            // but is still defined as an array for consistency with them.
            //   negativePattern: one of "(n)|-n|- n|n-|n -"
            pattern: ["-n"],
            // number of decimal places normally shown
            decimals: 2,
            // string that separates number groups, as in 1,000,000
            ",": ",",
            // string that separates a number from the fractional portion, as in 1.99
            ".": ".",
            // array of numbers indicating the size of each number group.
            // TODO: more detailed description and example
            groupSizes: [3],
            // symbol used for positive numbers
            "+": "+",
            // symbol used for negative numbers
            "-": "-",
            // symbol used for NaN (Not-A-Number)
            "NaN": "NaN",
            // symbol used for Negative Infinity
            negativeInfinity: "-Infinity",
            // symbol used for Positive Infinity
            positiveInfinity: "Infinity",
            percent: {
                // [negativePattern, positivePattern]
                //   negativePattern: one of "-n %|-n%|-%n|%-n|%n-|n-%|n%-|-% n|n %-|% n-|% -n|n- %"
                //   positivePattern: one of "n %|n%|%n|% n"
                pattern: ["-n %", "n %"],
                // number of decimal places normally shown
                decimals: 2,
                // array of numbers indicating the size of each number group.
                // TODO: more detailed description and example
                groupSizes: [3],
                // string that separates number groups, as in 1,000,000
                ",": ",",
                // string that separates a number from the fractional portion, as in 1.99
                ".": ".",
                // symbol used to represent a percentage
                symbol: "%"
            },
            currency: {
                // [negativePattern, positivePattern]
                //   negativePattern: one of "($n)|-$n|$-n|$n-|(n$)|-n$|n-$|n$-|-n $|-$ n|n $-|$ n-|$ -n|n- $|($ n)|(n $)"
                //   positivePattern: one of "$n|n$|$ n|n $"
                pattern: ["($n)", "$n"],
                // number of decimal places normally shown
                decimals: 2,
                // array of numbers indicating the size of each number group.
                // TODO: more detailed description and example
                groupSizes: [3],
                // string that separates number groups, as in 1,000,000
                ",": ",",
                // string that separates a number from the fractional portion, as in 1.99
                ".": ".",
                // symbol used to represent currency
                symbol: "$"
            }
        },
        // calendars defines all the possible calendars used by this culture.
        // There should be at least one defined with name "standard", and is the default
        // calendar used by the culture.
        // A calendar contains information about how dates are formatted, information about
        // the calendar's eras, a standard set of the date formats,
        // translations for day and month names, and if the calendar is not based on the Gregorian
        // calendar, conversion functions to and from the Gregorian calendar.
        calendars: {
            standard: {
                // name that identifies the type of calendar this is
                name: "Gregorian_USEnglish",
                // separator of parts of a date (e.g. "/" in 11/05/1955)
                "/": "/",
                // separator of parts of a time (e.g. ":" in 05:44 PM)
                ":": ":",
                // the first day of the week (0 = Sunday, 1 = Monday, etc)
                firstDay: 0,
                days: {
                    // full day names
                    names: ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"],
                    // abbreviated day names
                    namesAbbr: ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"],
                    // shortest day names
                    namesShort: ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"]
                },
                months: {
                    // full month names (13 months for lunar calendards -- 13th month should be "" if not lunar)
                    names: ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December", ""],
                    // abbreviated month names
                    namesAbbr: ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec", ""]
                },
                // AM and PM designators in one of these forms:
                // The usual view, and the upper and lower case versions
                //   [ standard, lowercase, uppercase ]
                // The culture does not use AM or PM (likely all standard date formats use 24 hour time)
                //   null
                AM: ["AM", "am", "AM"],
                PM: ["PM", "pm", "PM"],
                eras: [
                    // eras in reverse chronological order.
                    // name: the name of the era in this culture (e.g. A.D., C.E.)
                    // start: when the era starts in ticks (gregorian, gmt), null if it is the earliest supported era.
                    // offset: offset in years from gregorian calendar
                    {
                        "name": "A.D.",
                        "start": null,
                        "offset": 0
                    }
                ],
                // when a two digit year is given, it will never be parsed as a four digit
                // year greater than this year (in the appropriate era for the culture)
                // Set it as a full year (e.g. 2029) or use an offset format starting from
                // the current year: "+19" would correspond to 2029 if the current year 2010.
                twoDigitYearMax: 2029,
                // set of predefined date and time patterns used by the culture
                // these represent the format someone in this culture would expect
                // to see given the portions of the date that are shown.
                patterns: {
                    // short date pattern
                    d: "M/d/yyyy",
                    // long date pattern
                    D: "dddd, MMMM dd, yyyy",
                    // short time pattern
                    t: "h:mm tt",
                    // long time pattern
                    T: "h:mm:ss tt",
                    // long date, short time pattern
                    f: "dddd, MMMM dd, yyyy h:mm tt",
                    // long date, long time pattern
                    F: "dddd, MMMM dd, yyyy h:mm:ss tt",
                    // month/day pattern
                    M: "MMMM dd",
                    // month/year pattern
                    Y: "yyyy MMMM",
                    // S is a sortable format that does not vary by culture
                    S: "yyyy\u0027-\u0027MM\u0027-\u0027dd\u0027T\u0027HH\u0027:\u0027mm\u0027:\u0027ss"
                }
                // optional fields for each calendar:
                /*
                 monthsGenitive:
                 Same as months but used when the day preceeds the month.
                 Omit if the culture has no genitive distinction in month names.
                 For an explaination of genitive months, see http://blogs.msdn.com/michkap/archive/2004/12/25/332259.aspx
                 convert:
                 Allows for the support of non-gregorian based calendars. This convert object is used to
                 to convert a date to and from a gregorian calendar date to handle parsing and formatting.
                 The two functions:
                 fromGregorian( date )
                 Given the date as a parameter, return an array with parts [ year, month, day ]
                 corresponding to the non-gregorian based year, month, and day for the calendar.
                 toGregorian( year, month, day )
                 Given the non-gregorian year, month, and day, return a new Date() object
                 set to the corresponding date in the gregorian calendar.
                 */
            }
        },
        // For localized strings
        messages: {}
    };

    Globalize.cultures["default"].calendar = Globalize.cultures["default"].calendars.standard;

    Globalize.cultures.en = Globalize.cultures["default"];

    Globalize.cultureSelector = "en";

    //
    // private variables
    //

    regexHex = /^0x[a-f0-9]+$/i;
    regexInfinity = /^[+\-]?infinity$/i;
    regexParseFloat = /^[+\-]?\d*\.?\d*(e[+\-]?\d+)?$/;
    regexTrim = /^\s+|\s+$/g;

    //
    // private JavaScript utility functions
    //

    arrayIndexOf = function(array, item){
        if(array.indexOf) {
            return array.indexOf(item);
        }
        for(var i = 0, length = array.length; i < length; i++) {
            if(array[i] === item) {
                return i;
            }
        }
        return -1;
    };

    endsWith = function(value, pattern){
        return value.substr(value.length - pattern.length) === pattern;
    };

    extend = function(){
        var options, name, src, copy, copyIsArray, clone,
            target = arguments[0] || {},
            i = 1,
            length = arguments.length,
            deep = false;

        // Handle a deep copy situation
        if(typeof target === "boolean") {
            deep = target;
            target = arguments[1] || {};
            // skip the boolean and the target
            i = 2;
        }

        // Handle case when target is a string or something (possible in deep copy)
        if(typeof target !== "object" && !isFunction(target)) {
            target = {};
        }

        for(; i < length; i++) {
            // Only deal with non-null/undefined values
            if((options = arguments[i]) != null) {
                // Extend the base object
                for(name in options) {
                    src = target[name];
                    copy = options[name];

                    // Prevent never-ending loop
                    if(target === copy) {
                        continue;
                    }

                    // Recurse if we're merging plain objects or arrays
                    if(deep && copy && (isObject(copy) || (copyIsArray = isArray(copy)))) {
                        if(copyIsArray) {
                            copyIsArray = false;
                            clone = src && isArray(src) ? src : [];

                        } else {
                            clone = src && isObject(src) ? src : {};
                        }

                        // Never move original objects, clone them
                        target[name] = extend(deep, clone, copy);

                        // Don't bring in undefined values
                    } else if(copy !== undefined) {
                        target[name] = copy;
                    }
                }
            }
        }

        // Return the modified object
        return target;
    };

    isArray = Array.isArray || function(obj){
        return Object.prototype.toString.call(obj) === "[object Array]";
    };

    isFunction = function(obj){
        return Object.prototype.toString.call(obj) === "[object Function]";
    };

    isObject = function(obj){
        return Object.prototype.toString.call(obj) === "[object Object]";
    };

    startsWith = function(value, pattern){
        return value.indexOf(pattern) === 0;
    };

    trim = function(value){
        return (value + "").replace(regexTrim, "");
    };

    truncate = function(value){
        if(isNaN(value)) {
            return NaN;
        }
        return Math[value < 0 ? "ceil" : "floor"](value);
    };

    zeroPad = function(str, count, left){
        var l;
        for(l = str.length; l < count; l += 1) {
            str = (left ? ("0" + str) : (str + "0"));
        }
        return str;
    };

    //
    // private Globalization utility functions
    //

    appendPreOrPostMatch = function(preMatch, strings){
        // appends pre- and post- token match strings while removing escaped characters.
        // Returns a single quote count which is used to determine if the token occurs
        // in a string literal.
        var quoteCount = 0,
            escaped = false;
        for(var i = 0, il = preMatch.length; i < il; i++) {
            var c = preMatch.charAt(i);
            switch(c) {
            case "\'":
                if(escaped) {
                    strings.push("\'");
                }
                else {
                    quoteCount++;
                }
                escaped = false;
                break;
            case "\\":
                if(escaped) {
                    strings.push("\\");
                }
                escaped = !escaped;
                break;
            default:
                strings.push(c);
                escaped = false;
                break;
            }
        }
        return quoteCount;
    };

    expandFormat = function(cal, format){
        // expands unspecified or single character date formats into the full pattern.
        format = format || "F";
        var pattern,
            patterns = cal.patterns,
            len = format.length;
        if(len === 1) {
            pattern = patterns[format];
            if(!pattern) {
                throw "Invalid date format string \'" + format + "\'.";
            }
            format = pattern;
        }
        else if(len === 2 && format.charAt(0) === "%") {
            // %X escape format -- intended as a custom format string that is only one character, not a built-in format.
            format = format.charAt(1);
        }
        return format;
    };

    formatDate = function(value, format, culture){
        var cal = culture.calendar,
            convert = cal.convert,
            ret;

        if(!format || !format.length || format === "i") {
            if(culture && culture.name.length) {
                if(convert) {
                    // non-gregorian calendar, so we cannot use built-in toLocaleString()
                    ret = formatDate(value, cal.patterns.F, culture);
                }
                else {
                    var eraDate = new Date(value.getTime()),
                        era = getEra(value, cal.eras);
                    eraDate.setFullYear(getEraYear(value, cal, era));
                    ret = eraDate.toLocaleString();
                }
            }
            else {
                ret = value.toString();
            }
            return ret;
        }

        var eras = cal.eras,
            sortable = format === "s";
        format = expandFormat(cal, format);

        // Start with an empty string
        ret = [];
        var hour,
            zeros = ["0", "00", "000"],
            foundDay,
            checkedDay,
            dayPartRegExp = /([^d]|^)(d|dd)([^d]|$)/g,
            quoteCount = 0,
            tokenRegExp = getTokenRegExp(),
            converted;

        function padZeros(num, c){
            var r, s = num + "";
            if(c > 1 && s.length < c) {
                r = (zeros[c - 2] + s);
                return r.substr(r.length - c, c);
            }
            else {
                r = s;
            }
            return r;
        }

        function hasDay(){
            if(foundDay || checkedDay) {
                return foundDay;
            }
            foundDay = dayPartRegExp.test(format);
            checkedDay = true;
            return foundDay;
        }

        function getPart(date, part){
            if(converted) {
                return converted[part];
            }
            switch(part) {
            case 0:
                return date.getFullYear();
            case 1:
                return date.getMonth();
            case 2:
                return date.getDate();
            default:
                throw "Invalid part value " + part;
            }
        }

        if(!sortable && convert) {
            converted = convert.fromGregorian(value);
        }

        for(; ;) {
            // Save the current index
            var index = tokenRegExp.lastIndex,
                // Look for the next pattern
                ar = tokenRegExp.exec(format);

            // Append the text before the pattern (or the end of the string if not found)
            var preMatch = format.slice(index, ar ? ar.index : format.length);
            quoteCount += appendPreOrPostMatch(preMatch, ret);

            if(!ar) {
                break;
            }

            // do not replace any matches that occur inside a string literal.
            if(quoteCount % 2) {
                ret.push(ar[0]);
                continue;
            }

            var current = ar[0],
                clength = current.length;

            switch(current) {
            case "ddd":
                //Day of the week, as a three-letter abbreviation
            case "dddd":
                // Day of the week, using the full name
                var names = (clength === 3) ? cal.days.namesAbbr : cal.days.names;
                ret.push(names[value.getDay()]);
                break;
            case "d":
                // Day of month, without leading zero for single-digit days
            case "dd":
                // Day of month, with leading zero for single-digit days
                foundDay = true;
                ret.push(
                    padZeros(getPart(value, 2), clength)
                );
                break;
            case "MMM":
                // Month, as a three-letter abbreviation
            case "MMMM":
                // Month, using the full name
                var part = getPart(value, 1);
                ret.push(
                    (cal.monthsGenitive && hasDay()) ?
                        (cal.monthsGenitive[clength === 3 ? "namesAbbr" : "names"][part]) :
                        (cal.months[clength === 3 ? "namesAbbr" : "names"][part])
                );
                break;
            case "M":
                // Month, as digits, with no leading zero for single-digit months
            case "MM":
                // Month, as digits, with leading zero for single-digit months
                ret.push(
                    padZeros(getPart(value, 1) + 1, clength)
                );
                break;
            case "y":
                // Year, as two digits, but with no leading zero for years less than 10
            case "yy":
                // Year, as two digits, with leading zero for years less than 10
            case "yyyy":
                // Year represented by four full digits
                part = converted ? converted[0] : getEraYear(value, cal, getEra(value, eras), sortable);
                if(clength < 4) {
                    part = part % 100;
                }
                ret.push(
                    padZeros(part, clength)
                );
                break;
            case "h":
                // Hours with no leading zero for single-digit hours, using 12-hour clock
            case "hh":
                // Hours with leading zero for single-digit hours, using 12-hour clock
                hour = value.getHours() % 12;
                if(hour === 0) {
                    hour = 12;
                }
                ret.push(
                    padZeros(hour, clength)
                );
                break;
            case "H":
                // Hours with no leading zero for single-digit hours, using 24-hour clock
            case "HH":
                // Hours with leading zero for single-digit hours, using 24-hour clock
                ret.push(
                    padZeros(value.getHours(), clength)
                );
                break;
            case "m":
                // Minutes with no leading zero for single-digit minutes
            case "mm":
                // Minutes with leading zero for single-digit minutes
                ret.push(
                    padZeros(value.getMinutes(), clength)
                );
                break;
            case "s":
                // Seconds with no leading zero for single-digit seconds
            case "ss":
                // Seconds with leading zero for single-digit seconds
                ret.push(
                    padZeros(value.getSeconds(), clength)
                );
                break;
            case "t":
                // One character am/pm indicator ("a" or "p")
            case "tt":
                // Multicharacter am/pm indicator
                part = value.getHours() < 12 ? (cal.AM ? cal.AM[0] : " ") : (cal.PM ? cal.PM[0] : " ");
                ret.push(clength === 1 ? part.charAt(0) : part);
                break;
            case "f":
                // Deciseconds
            case "ff":
                // Centiseconds
            case "fff":
                // Milliseconds
                ret.push(
                    padZeros(value.getMilliseconds(), 3).substr(0, clength)
                );
                break;
            case "z":
                // Time zone offset, no leading zero
            case "zz":
                // Time zone offset with leading zero
                hour = value.getTimezoneOffset() / 60;
                ret.push(
                    (hour <= 0 ? "+" : "-") + padZeros(Math.floor(Math.abs(hour)), clength)
                );
                break;
            case "zzz":
                // Time zone offset with leading zero
                hour = value.getTimezoneOffset() / 60;
                ret.push(
                    (hour <= 0 ? "+" : "-") + padZeros(Math.floor(Math.abs(hour)), 2) +
                    // Hard coded ":" separator, rather than using cal.TimeSeparator
                    // Repeated here for consistency, plus ":" was already assumed in date parsing.
                    ":" + padZeros(Math.abs(value.getTimezoneOffset() % 60), 2)
                );
                break;
            case "g":
            case "gg":
                if(cal.eras) {
                    ret.push(
                        cal.eras[getEra(value, eras)].name
                    );
                }
                break;
            case "/":
                ret.push(cal["/"]);
                break;
            default:
                throw "Invalid date format pattern \'" + current + "\'.";
            }
        }
        return ret.join("");
    };

    // formatNumber
    (function(){
        var expandNumber;

        expandNumber = function(number, precision, formatInfo){
            var groupSizes = formatInfo.groupSizes,
                curSize = groupSizes[0],
                curGroupIndex = 1,
                factor = Math.pow(10, precision),
                rounded = Math.round(number * factor) / factor;

            if(!isFinite(rounded)) {
                rounded = number;
            }
            number = rounded;

            var numberString = number + "",
                right = "",
                split = numberString.split(/e/i),
                exponent = split.length > 1 ? parseInt(split[1], 10) : 0;
            numberString = split[0];
            split = numberString.split(".");
            numberString = split[0];
            right = split.length > 1 ? split[1] : "";

            var l;
            if(exponent > 0) {
                right = zeroPad(right, exponent, false);
                numberString += right.slice(0, exponent);
                right = right.substr(exponent);
            }
            else if(exponent < 0) {
                exponent = -exponent;
                numberString = zeroPad(numberString, exponent + 1, true);
                right = numberString.slice(-exponent, numberString.length) + right;
                numberString = numberString.slice(0, -exponent);
            }

            if(precision > 0) {
                right = formatInfo["."] +
                    ((right.length > precision) ? right.slice(0, precision) : zeroPad(right, precision));
            }
            else {
                right = "";
            }

            var stringIndex = numberString.length - 1,
                sep = formatInfo[","],
                ret = "";

            while(stringIndex >= 0) {
                if(curSize === 0 || curSize > stringIndex) {
                    return numberString.slice(0, stringIndex + 1) + (ret.length ? (sep + ret + right) : right);
                }
                ret = numberString.slice(stringIndex - curSize + 1, stringIndex + 1) + (ret.length ? (sep + ret) : "");

                stringIndex -= curSize;

                if(curGroupIndex < groupSizes.length) {
                    curSize = groupSizes[curGroupIndex];
                    curGroupIndex++;
                }
            }

            return numberString.slice(0, stringIndex + 1) + sep + ret + right;
        };

        formatNumber = function(value, format, culture){
            if(!isFinite(value)) {
                if(value === Infinity) {
                    return culture.numberFormat.positiveInfinity;
                }
                if(value === -Infinity) {
                    return culture.numberFormat.negativeInfinity;
                }
                return culture.numberFormat["NaN"];
            }
            if(!format || format === "i") {
                return culture.name.length ? value.toLocaleString() : value.toString();
            }
            format = format || "D";

            var nf = culture.numberFormat,
                number = Math.abs(value),
                precision = -1,
                pattern;
            if(format.length > 1) {
                precision = parseInt(format.slice(1), 10);
            }

            var current = format.charAt(0).toUpperCase(),
                formatInfo;

            switch(current) {
            case "D":
                pattern = "n";
                number = truncate(number);
                if(precision !== -1) {
                    number = zeroPad("" + number, precision, true);
                }
                if(value < 0) {
                    number = "-" + number;
                }
                break;
            case "N":
                formatInfo = nf;
                /* falls through */
            case "C":
                formatInfo = formatInfo || nf.currency;
                /* falls through */
            case "P":
                formatInfo = formatInfo || nf.percent;
                pattern = value < 0 ? formatInfo.pattern[0] : (formatInfo.pattern[1] || "n");
                if(precision === -1) {
                    precision = formatInfo.decimals;
                }
                number = expandNumber(number * (current === "P" ? 100 : 1), precision, formatInfo);
                break;
            default:
                throw "Bad number format specifier: " + current;
            }

            var patternParts = /n|\$|-|%/g,
                ret = "";
            for(; ;) {
                var index = patternParts.lastIndex,
                    ar = patternParts.exec(pattern);

                ret += pattern.slice(index, ar ? ar.index : pattern.length);

                if(!ar) {
                    break;
                }

                switch(ar[0]) {
                case "n":
                    ret += number;
                    break;
                case "$":
                    ret += nf.currency.symbol;
                    break;
                case "-":
                    // don't make 0 negative
                    if(/[1-9]/.test(number)) {
                        ret += nf["-"];
                    }
                    break;
                case "%":
                    ret += nf.percent.symbol;
                    break;
                }
            }

            return ret;
        };

    }());

    getTokenRegExp = function(){
        // regular expression for matching date and time tokens in format strings.
        return (/\/|dddd|ddd|dd|d|MMMM|MMM|MM|M|yyyy|yy|y|hh|h|HH|H|mm|m|ss|s|tt|t|fff|ff|f|zzz|zz|z|gg|g/g);
    };

    getEra = function(date, eras){
        if(!eras) {
            return 0;
        }
        var start, ticks = date.getTime();
        for(var i = 0, l = eras.length; i < l; i++) {
            start = eras[i].start;
            if(start === null || ticks >= start) {
                return i;
            }
        }
        return 0;
    };

    getEraYear = function(date, cal, era, sortable){
        var year = date.getFullYear();
        if(!sortable && cal.eras) {
            // convert normal gregorian year to era-shifted gregorian
            // year by subtracting the era offset
            year -= cal.eras[era].offset;
        }
        return year;
    };

    // parseExact
    (function(){
        var expandYear,
            getDayIndex,
            getMonthIndex,
            getParseRegExp,
            outOfRange,
            toUpper,
            toUpperArray;

        expandYear = function(cal, year){
            // expands 2-digit year into 4 digits.
            if(year < 100) {
                var now = new Date(),
                    era = getEra(now),
                    curr = getEraYear(now, cal, era),
                    twoDigitYearMax = cal.twoDigitYearMax;
                twoDigitYearMax = typeof twoDigitYearMax === "string" ? new Date().getFullYear() % 100 + parseInt(twoDigitYearMax, 10) : twoDigitYearMax;
                year += curr - (curr % 100);
                if(year > twoDigitYearMax) {
                    year -= 100;
                }
            }
            return year;
        };

        getDayIndex = function(cal, value, abbr){
            var ret,
                days = cal.days,
                upperDays = cal._upperDays;
            if(!upperDays) {
                cal._upperDays = upperDays = [
                    toUpperArray(days.names),
                    toUpperArray(days.namesAbbr),
                    toUpperArray(days.namesShort)
                ];
            }
            value = toUpper(value);
            if(abbr) {
                ret = arrayIndexOf(upperDays[1], value);
                if(ret === -1) {
                    ret = arrayIndexOf(upperDays[2], value);
                }
            }
            else {
                ret = arrayIndexOf(upperDays[0], value);
            }
            return ret;
        };

        getMonthIndex = function(cal, value, abbr){
            var months = cal.months,
                monthsGen = cal.monthsGenitive || cal.months,
                upperMonths = cal._upperMonths,
                upperMonthsGen = cal._upperMonthsGen;
            if(!upperMonths) {
                cal._upperMonths = upperMonths = [
                    toUpperArray(months.names),
                    toUpperArray(months.namesAbbr)
                ];
                cal._upperMonthsGen = upperMonthsGen = [
                    toUpperArray(monthsGen.names),
                    toUpperArray(monthsGen.namesAbbr)
                ];
            }
            value = toUpper(value);
            var i = arrayIndexOf(abbr ? upperMonths[1] : upperMonths[0], value);
            if(i < 0) {
                i = arrayIndexOf(abbr ? upperMonthsGen[1] : upperMonthsGen[0], value);
            }
            return i;
        };

        getParseRegExp = function(cal, format){
            // converts a format string into a regular expression with groups that
            // can be used to extract date fields from a date string.
            // check for a cached parse regex.
            var re = cal._parseRegExp;
            if(!re) {
                cal._parseRegExp = re = {};
            }
            else {
                var reFormat = re[format];
                if(reFormat) {
                    return reFormat;
                }
            }

            // expand single digit formats, then escape regular expression characters.
            var expFormat = expandFormat(cal, format).replace(/([\^\$\.\*\+\?\|\[\]\(\)\{\}])/g, "\\\\$1"),
                regexp = ["^"],
                groups = [],
                index = 0,
                quoteCount = 0,
                tokenRegExp = getTokenRegExp(),
                match;

            // iterate through each date token found.
            while((match = tokenRegExp.exec(expFormat)) !== null) {
                var preMatch = expFormat.slice(index, match.index);
                index = tokenRegExp.lastIndex;

                // don't replace any matches that occur inside a string literal.
                quoteCount += appendPreOrPostMatch(preMatch, regexp);
                if(quoteCount % 2) {
                    regexp.push(match[0]);
                    continue;
                }

                // add a regex group for the token.
                var m = match[0], len = m.length, add;
                switch(m) {
                case "dddd":
                case "ddd":
                case "MMMM":
                case "MMM":
                case "gg":
                case "g":
                    add = "(\\D+)";
                    break;
                case "tt":
                case "t":
                    add = "(\\D*)";
                    break;
                case "yyyy":
                case "fff":
                case "ff":
                case "f":
                    add = "(\\d{" + len + "})";
                    break;
                case "dd":
                case "d":
                case "MM":
                case "M":
                case "yy":
                case "y":
                case "HH":
                case "H":
                case "hh":
                case "h":
                case "mm":
                case "m":
                case "ss":
                case "s":
                    add = "(\\d\\d?)";
                    break;
                case "zzz":
                    add = "([+-]?\\d\\d?:\\d{2})";
                    break;
                case "zz":
                case "z":
                    add = "([+-]?\\d\\d?)";
                    break;
                case "/":
                    add = "(\\/)";
                    break;
                default:
                    throw "Invalid date format pattern \'" + m + "\'.";
                }
                if(add) {
                    regexp.push(add);
                }
                groups.push(match[0]);
            }
            appendPreOrPostMatch(expFormat.slice(index), regexp);
            regexp.push("$");

            // allow whitespace to differ when matching formats.
            var regexpStr = regexp.join("").replace(/\s+/g, "\\s+"),
                parseRegExp = {"regExp": regexpStr, "groups": groups};

            // cache the regex for this format.
            return re[format] = parseRegExp;
        };

        outOfRange = function(value, low, high){
            return value < low || value > high;
        };

        toUpper = function(value){
            // "he-IL" has non-breaking space in weekday names.
            return value.split("\u00A0").join(" ").toUpperCase();
        };

        toUpperArray = function(arr){
            var results = [];
            for(var i = 0, l = arr.length; i < l; i++) {
                results[i] = toUpper(arr[i]);
            }
            return results;
        };

        parseExact = function(value, format, culture){
            // try to parse the date string by matching against the format string
            // while using the specified culture for date field names.
            value = trim(value);
            var cal = culture.calendar,
                // convert date formats into regular expressions with groupings.
                // use the regexp to determine the input format and extract the date fields.
                parseInfo = getParseRegExp(cal, format),
                match = new RegExp(parseInfo.regExp).exec(value);
            if(match === null) {
                return null;
            }
            // found a date format that matches the input.
            var groups = parseInfo.groups,
                era = null, year = null, month = null, date = null, weekDay = null,
                hour = 0, hourOffset, min = 0, sec = 0, msec = 0, tzMinOffset = null,
                pmHour = false;
            // iterate the format groups to extract and set the date fields.
            for(var j = 0, jl = groups.length; j < jl; j++) {
                var matchGroup = match[j + 1];
                if(matchGroup) {
                    var current = groups[j],
                        clength = current.length,
                        matchInt = parseInt(matchGroup, 10);
                    switch(current) {
                    case "dd":
                    case "d":
                        // Day of month.
                        date = matchInt;
                        // check that date is generally in valid range, also checking overflow below.
                        if(outOfRange(date, 1, 31)) {
                            return null;
                        }
                        break;
                    case "MMM":
                    case "MMMM":
                        month = getMonthIndex(cal, matchGroup, clength === 3);
                        if(outOfRange(month, 0, 11)) {
                            return null;
                        }
                        break;
                    case "M":
                    case "MM":
                        // Month.
                        month = matchInt - 1;
                        if(outOfRange(month, 0, 11)) {
                            return null;
                        }
                        break;
                    case "y":
                    case "yy":
                    case "yyyy":
                        year = clength < 4 ? expandYear(cal, matchInt) : matchInt;
                        if(outOfRange(year, 0, 9999)) {
                            return null;
                        }
                        break;
                    case "h":
                    case "hh":
                        // Hours (12-hour clock).
                        hour = matchInt;
                        if(hour === 12) {
                            hour = 0;
                        }
                        if(outOfRange(hour, 0, 11)) {
                            return null;
                        }
                        break;
                    case "H":
                    case "HH":
                        // Hours (24-hour clock).
                        hour = matchInt;
                        if(outOfRange(hour, 0, 23)) {
                            return null;
                        }
                        break;
                    case "m":
                    case "mm":
                        // Minutes.
                        min = matchInt;
                        if(outOfRange(min, 0, 59)) {
                            return null;
                        }
                        break;
                    case "s":
                    case "ss":
                        // Seconds.
                        sec = matchInt;
                        if(outOfRange(sec, 0, 59)) {
                            return null;
                        }
                        break;
                    case "tt":
                    case "t":
                        // AM/PM designator.
                        // see if it is standard, upper, or lower case PM. If not, ensure it is at least one of
                        // the AM tokens. If not, fail the parse for this format.
                        pmHour = cal.PM && (matchGroup === cal.PM[0] || matchGroup === cal.PM[1] || matchGroup === cal.PM[2]);
                        if(
                            !pmHour && (
                                !cal.AM || (matchGroup !== cal.AM[0] && matchGroup !== cal.AM[1] && matchGroup !== cal.AM[2])
                            )
                        ) {
                            return null;
                        }
                        break;
                    case "f":
                        // Deciseconds.
                    case "ff":
                        // Centiseconds.
                    case "fff":
                        // Milliseconds.
                        msec = matchInt * Math.pow(10, 3 - clength);
                        if(outOfRange(msec, 0, 999)) {
                            return null;
                        }
                        break;
                    case "ddd":
                        // Day of week.
                    case "dddd":
                        // Day of week.
                        weekDay = getDayIndex(cal, matchGroup, clength === 3);
                        if(outOfRange(weekDay, 0, 6)) {
                            return null;
                        }
                        break;
                    case "zzz":
                        // Time zone offset in +/- hours:min.
                        var offsets = matchGroup.split(/:/);
                        if(offsets.length !== 2) {
                            return null;
                        }
                        hourOffset = parseInt(offsets[0], 10);
                        if(outOfRange(hourOffset, -12, 13)) {
                            return null;
                        }
                        var minOffset = parseInt(offsets[1], 10);
                        if(outOfRange(minOffset, 0, 59)) {
                            return null;
                        }
                        tzMinOffset = (hourOffset * 60) + (startsWith(matchGroup, "-") ? -minOffset : minOffset);
                        break;
                    case "z":
                    case "zz":
                        // Time zone offset in +/- hours.
                        hourOffset = matchInt;
                        if(outOfRange(hourOffset, -12, 13)) {
                            return null;
                        }
                        tzMinOffset = hourOffset * 60;
                        break;
                    case "g":
                    case "gg":
                        var eraName = matchGroup;
                        if(!eraName || !cal.eras) {
                            return null;
                        }
                        eraName = trim(eraName.toLowerCase());
                        for(var i = 0, l = cal.eras.length; i < l; i++) {
                            if(eraName === cal.eras[i].name.toLowerCase()) {
                                era = i;
                                break;
                            }
                        }
                        // could not find an era with that name
                        if(era === null) {
                            return null;
                        }
                        break;
                    }
                }
            }
            var result = new Date(), defaultYear, convert = cal.convert;
            defaultYear = convert ? convert.fromGregorian(result)[0] : result.getFullYear();
            if(year === null) {
                year = defaultYear;
            }
            else if(cal.eras) {
                // year must be shifted to normal gregorian year
                // but not if year was not specified, its already normal gregorian
                // per the main if clause above.
                year += cal.eras[(era || 0)].offset;
            }
            // set default day and month to 1 and January, so if unspecified, these are the defaults
            // instead of the current day/month.
            if(month === null) {
                month = 0;
            }
            if(date === null) {
                date = 1;
            }
            // now have year, month, and date, but in the culture's calendar.
            // convert to gregorian if necessary
            if(convert) {
                result = convert.toGregorian(year, month, date);
                // conversion failed, must be an invalid match
                if(result === null) {
                    return null;
                }
            }
            else {
                // have to set year, month and date together to avoid overflow based on current date.
                result.setFullYear(year, month, date);
                // check to see if date overflowed for specified month (only checked 1-31 above).
                if(result.getDate() !== date) {
                    return null;
                }
                // invalid day of week.
                if(weekDay !== null && result.getDay() !== weekDay) {
                    return null;
                }
            }
            // if pm designator token was found make sure the hours fit the 24-hour clock.
            if(pmHour && hour < 12) {
                hour += 12;
            }
            result.setHours(hour, min, sec, msec);
            if(tzMinOffset !== null) {
                // adjust timezone to utc before applying local offset.
                var adjustedMin = result.getMinutes() - (tzMinOffset + result.getTimezoneOffset());
                // Safari limits hours and minutes to the range of -127 to 127.  We need to use setHours
                // to ensure both these fields will not exceed this range.	adjustedMin will range
                // somewhere between -1440 and 1500, so we only need to split this into hours.
                result.setHours(result.getHours() + parseInt(adjustedMin / 60, 10), adjustedMin % 60);
            }
            return result;
        };
    }());

    parseNegativePattern = function(value, nf, negativePattern){
        var neg = nf["-"],
            pos = nf["+"],
            ret;
        switch(negativePattern) {
        case "n -":
            neg = " " + neg;
            pos = " " + pos;
            /* falls through */
        case "n-":
            if(endsWith(value, neg)) {
                ret = ["-", value.substr(0, value.length - neg.length)];
            }
            else if(endsWith(value, pos)) {
                ret = ["+", value.substr(0, value.length - pos.length)];
            }
            break;
        case "- n":
            neg += " ";
            pos += " ";
            /* falls through */
        case "-n":
            if(startsWith(value, neg)) {
                ret = ["-", value.substr(neg.length)];
            }
            else if(startsWith(value, pos)) {
                ret = ["+", value.substr(pos.length)];
            }
            break;
        case "(n)":
            if(startsWith(value, "(") && endsWith(value, ")")) {
                ret = ["-", value.substr(1, value.length - 2)];
            }
            break;
        }
        return ret || ["", value];
    };

    //
    // public instance functions
    //

    Globalize.prototype.findClosestCulture = function(cultureSelector){
        return Globalize.findClosestCulture.call(this, cultureSelector);
    };

    Globalize.prototype.format = function(value, format, cultureSelector){
        return Globalize.format.call(this, value, format, cultureSelector);
    };

    Globalize.prototype.localize = function(key, cultureSelector){
        return Globalize.localize.call(this, key, cultureSelector);
    };

    Globalize.prototype.parseInt = function(value, radix, cultureSelector){
        return Globalize.parseInt.call(this, value, radix, cultureSelector);
    };

    Globalize.prototype.parseFloat = function(value, radix, cultureSelector){
        return Globalize.parseFloat.call(this, value, radix, cultureSelector);
    };

    Globalize.prototype.culture = function(cultureSelector){
        return Globalize.culture.call(this, cultureSelector);
    };

    //
    // public singleton functions
    //

    Globalize.addCultureInfo = function(cultureName, baseCultureName, info){

        var base = {},
            isNew = false;

        if(typeof cultureName !== "string") {
            // cultureName argument is optional string. If not specified, assume info is first
            // and only argument. Specified info deep-extends current culture.
            info = cultureName;
            cultureName = this.culture().name;
            base = this.cultures[cultureName];
        } else if(typeof baseCultureName !== "string") {
            // baseCultureName argument is optional string. If not specified, assume info is second
            // argument. Specified info deep-extends specified culture.
            // If specified culture does not exist, create by deep-extending default
            info = baseCultureName;
            isNew = (this.cultures[cultureName] == null);
            base = this.cultures[cultureName] || this.cultures["default"];
        } else {
            // cultureName and baseCultureName specified. Assume a new culture is being created
            // by deep-extending an specified base culture
            isNew = true;
            base = this.cultures[baseCultureName];
        }

        this.cultures[cultureName] = extend(true, {},
            base,
            info
        );
        // Make the standard calendar the current culture if it's a new culture
        if(isNew) {
            this.cultures[cultureName].calendar = this.cultures[cultureName].calendars.standard;
        }
    };

    Globalize.findClosestCulture = function(name){
        var match;
        if(!name) {
            return this.findClosestCulture(this.cultureSelector) || this.cultures["default"];
        }
        if(typeof name === "string") {
            name = name.split(",");
        }
        if(isArray(name)) {
            var lang,
                cultures = this.cultures,
                list = name,
                i, l = list.length,
                prioritized = [];
            for(i = 0; i < l; i++) {
                name = trim(list[i]);
                var pri, parts = name.split(";");
                lang = trim(parts[0]);
                if(parts.length === 1) {
                    pri = 1;
                }
                else {
                    name = trim(parts[1]);
                    if(name.indexOf("q=") === 0) {
                        name = name.substr(2);
                        pri = parseFloat(name);
                        pri = isNaN(pri) ? 0 : pri;
                    }
                    else {
                        pri = 1;
                    }
                }
                prioritized.push({lang: lang, pri: pri});
            }
            prioritized.sort(function(a, b){
                if(a.pri < b.pri) {
                    return 1;
                } else if(a.pri > b.pri) {
                    return -1;
                }
                return 0;
            });
            // exact match
            for(i = 0; i < l; i++) {
                lang = prioritized[i].lang;
                match = cultures[lang];
                if(match) {
                    return match;
                }
            }

            // neutral language match
            for(i = 0; i < l; i++) {
                lang = prioritized[i].lang;
                do {
                    var index = lang.lastIndexOf("-");
                    if(index === -1) {
                        break;
                    }
                    // strip off the last part. e.g. en-US => en
                    lang = lang.substr(0, index);
                    match = cultures[lang];
                    if(match) {
                        return match;
                    }
                }
                while(1);
            }

            // last resort: match first culture using that language
            for(i = 0; i < l; i++) {
                lang = prioritized[i].lang;
                for(var cultureKey in cultures) {
                    var culture = cultures[cultureKey];
                    if(culture.language == lang) {
                        return culture;
                    }
                }
            }
        }
        else if(typeof name === "object") {
            return name;
        }
        return match || null;
    };

    Globalize.format = function(value, format, cultureSelector){
        var culture = this.findClosestCulture(cultureSelector);
        if(value instanceof Date) {
            value = formatDate(value, format, culture);
        }
        else if(typeof value === "number") {
            value = formatNumber(value, format, culture);
        }
        return value;
    };

    Globalize.localize = function(key, cultureSelector){
        return this.findClosestCulture(cultureSelector).messages[key] ||
            this.cultures["default"].messages[key];
    };

    Globalize.parseDate = function(value, formats, culture){
        culture = this.findClosestCulture(culture);

        var date, prop, patterns;
        if(formats) {
            if(typeof formats === "string") {
                formats = [formats];
            }
            if(formats.length) {
                for(var i = 0, l = formats.length; i < l; i++) {
                    var format = formats[i];
                    if(format) {
                        date = parseExact(value, format, culture);
                        if(date) {
                            break;
                        }
                    }
                }
            }
        } else {
            patterns = culture.calendar.patterns;
            for(prop in patterns) {
                date = parseExact(value, patterns[prop], culture);
                if(date) {
                    break;
                }
            }
        }

        return date || null;
    };

    Globalize.parseInt = function(value, radix, cultureSelector){
        return truncate(Globalize.parseFloat(value, radix, cultureSelector));
    };

    Globalize.parseFloat = function(value, radix, cultureSelector){
        // radix argument is optional
        if(typeof radix !== "number") {
            cultureSelector = radix;
            radix = 10;
        }

        var culture = this.findClosestCulture(cultureSelector);
        var ret = NaN,
            nf = culture.numberFormat;

        if(value.indexOf(culture.numberFormat.currency.symbol) > -1) {
            // remove currency symbol
            value = value.replace(culture.numberFormat.currency.symbol, "");
            // replace decimal seperator
            value = value.replace(culture.numberFormat.currency["."], culture.numberFormat["."]);
        }

        //Remove percentage character from number string before parsing
        if(value.indexOf(culture.numberFormat.percent.symbol) > -1) {
            value = value.replace(culture.numberFormat.percent.symbol, "");
        }

        // remove spaces: leading, trailing and between - and number. Used for negative currency pt-BR
        value = value.replace(/ /g, "");

        // allow infinity or hexidecimal
        if(regexInfinity.test(value)) {
            ret = parseFloat(value);
        }
        else if(!radix && regexHex.test(value)) {
            ret = parseInt(value, 16);
        }
        else {

            // determine sign and number
            var signInfo = parseNegativePattern(value, nf, nf.pattern[0]),
                sign = signInfo[0],
                num = signInfo[1];

            // #44 - try parsing as "(n)"
            if(sign === "" && nf.pattern[0] !== "(n)") {
                signInfo = parseNegativePattern(value, nf, "(n)");
                sign = signInfo[0];
                num = signInfo[1];
            }

            // try parsing as "-n"
            if(sign === "" && nf.pattern[0] !== "-n") {
                signInfo = parseNegativePattern(value, nf, "-n");
                sign = signInfo[0];
                num = signInfo[1];
            }

            sign = sign || "+";

            // determine exponent and number
            var exponent,
                intAndFraction,
                exponentPos = num.indexOf("e");
            if(exponentPos < 0) {
                exponentPos = num.indexOf("E");
            }
            if(exponentPos < 0) {
                intAndFraction = num;
                exponent = null;
            }
            else {
                intAndFraction = num.substr(0, exponentPos);
                exponent = num.substr(exponentPos + 1);
            }
            // determine decimal position
            var integer,
                fraction,
                decSep = nf["."],
                decimalPos = intAndFraction.indexOf(decSep);
            if(decimalPos < 0) {
                integer = intAndFraction;
                fraction = null;
            }
            else {
                integer = intAndFraction.substr(0, decimalPos);
                fraction = intAndFraction.substr(decimalPos + decSep.length);
            }
            // handle groups (e.g. 1,000,000)
            var groupSep = nf[","];
            integer = integer.split(groupSep).join("");
            var altGroupSep = groupSep.replace(/\u00A0/g, " ");
            if(groupSep !== altGroupSep) {
                integer = integer.split(altGroupSep).join("");
            }
            // build a natively parsable number string
            var p = sign + integer;
            if(fraction !== null) {
                p += "." + fraction;
            }
            if(exponent !== null) {
                // exponent itself may have a number patternd
                var expSignInfo = parseNegativePattern(exponent, nf, "-n");
                p += "e" + (expSignInfo[0] || "+") + expSignInfo[1];
            }
            if(regexParseFloat.test(p)) {
                ret = parseFloat(p);
            }
        }
        return ret;
    };

    Globalize.culture = function(cultureSelector){
        // setter
        if(typeof cultureSelector !== "undefined") {
            this.cultureSelector = cultureSelector;
        }
        // getter
        return this.findClosestCulture(cultureSelector) || this.cultures["default"];
    };

}(this));
